09.2 精通自定义 View 之 Canvas 与图层——图层与画布

返回自定义 View 目录

前面讲过 Canvas 的 save() 和 restore() 函数,除这两个函数以外,还有其他一些函数来保存和恢复画布状态。

9.2.1 saveLayer() 函数

saveLayer 有两个构造函数,如下:

1
2
3
4
5
6
/**
* 保存指定矩形区域的canvas内容
*/
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayer(float left, float top, float right, float bottom,
Paint paint, int saveFlags)

参数:

  • RectF bounds:要保存的区域所对应的矩形对象。
  • int saveFlags:取值有 ALL_SAVE_FLAG、MATRIX_SAVE_FLAG、CLIP_SAVE_FLAG、HAS_ALPHA_LAYER_SAVE_FLAG、FULL_COLOR_LAYER_SAVE_FLAG、 和
    CLIP_TO_LAYER_SAVE_FLAG,其中 ALL_SAVE_FLAG 表示保存全部内容,这些标识的具体意义我们后面会具体讲。

第二个构造函数实际与第一个是一样的,只不过它是根据 4 个点来构造一个矩形。下面以 Xfermode 为例,来看看 saveLayer() 函数都做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class TestView extends View {
private Paint mPaint;
private Bitmap dstBmp;
private Bitmap srcBmp;
private int width = 400;
private int height = 400;
private PorterDuffXfermode mMode;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
dstBmp = makeBitmap(width, height, 0xFFFFCC44, "oval");
srcBmp = makeBitmap(width, height, 0xFF66AAFF, "rect");
mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
private Bitmap makeBitmap(int w, int h, int color, String type) {
Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
if ("oval".equals(type)) {
canvas.drawOval(new RectF(0, 0, w, h), paint);
} else {
canvas.drawRect(0, 0, w, h, paint);
}
return bmp;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
int layerId = canvas.saveLayer(0, 0, width * 2, height * 2,
null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(mMode);
canvas.drawBitmap(srcBmp, width / 2f, height / 2f, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}

这段代码我们应该很熟悉,这是在讲解 setXfermode() 函数时的示例代码,但在调用 saveLayer() 函数前把整个屏幕画成了绿色,效果图如下。

那么问题来了,如果我们把 saveLayer() 函数去掉,则会怎样?代码如下:

1
2
3
4
5
6
7
8
9
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(mMode);
canvas.drawBitmap(srcBmp, width / 2f, height / 2f, mPaint);
mPaint.setXfermode(null);
}

效果如下图所示。

可以看到,效果居然不一样。先来回顾下 Mode.SRC_IN 模式的效果:在处理源图像时,以显示源图像为主,在相交时利用目标图像的透明度来改变源图像的透明度和饱和度;当目标图像透明度为 0 时,源图像就完全不显示。

再回过来看结果,第一个结果是对的,因为除与圆相交以外的区域透明度都是 0,而第二个结果怎么变成了这样,源图像为什么全部都显示出来了?

1. 调用 saveLayer() 函数时的绘图流程

在调用 saveLayer() 函数时,会生成一个全新的画布(Bitmap),这块画布的大小就是我们指定的所要保存区域的大小。新生成的画布是全透明的,在调用 saveLayer() 函数后所有的绘图操作都是在这个画布上进行的。

我们讲过,在利用 Xfermode 画源图像时,会把之前画布上所有的内容都做为目标图像,而在调用 saveLayer() 函数新生成的画布上,只有 dstBmp 对应的圆形。所以,除与圆形相交之外的位置都是空白像素。

对于 Xfermode 而言,在绘图完成之后,会把调用 saveLayer() 函数所生成的透明画布覆盖在原来的画布上面,以形成最终的显示结果。

此时的 Xfermode 的合成过程如下图所示。

中间的透明画布就是调用 saveLayer() 函数自动生成的,最上方的透明图层是调用 drawBitmap() 函数生成的。我们知道,每次调用 canvas.drawXXX 系列函数,都会生成一个透明层来专门绘制这个图形,而每次生成的图层都会叠加到最近的画布上。因为我们在这里对源图像应用了 Xfermode 算法,所以在叠加到就近的调用 saveLayer() 函数生成的画布上时,会进行计算。在新建的画布上绘制完成以后,整体覆盖在原始画布上显示出来。

正是因为在使用 Xfermode 计算时,目标图像是绘制在新建的透明画布上的,所以除圆形以外的区域全部是透明像素,最终的显示结果是正确的。

2. 没有 saveLayer() 函数时的绘图流程

在第二个示例中,唯一不同的就是把 saveLayer() 函数去掉了。

在去掉 saveLayer() 函数后,就不会新建画布了。当然,所有的绘图操作都会在原始画布上进行。

由于先把整块画布染成了绿色,再画上一个圆形,所有在应用 Xfermode 来画源图像的时候,在目标画布上是没有透明像素的。这也就不难解释结果为什么是这样的。

此时的 Xfermode 合成过程如下图所示。

由于没有调用 saveLayer() 函数,所以圆形是直接画在原始画布上的,而当矩形与其相交时,就是直接与原始画布上的所有图像做计算的。

结论:调用 saveLayer() 函数会创建一个全新的透明画布,大小与指定保存的区域大小一致,其后的绘图操作都放在这块画布上进行。在绘制结束后,会直接盖在原始画布上显示。

9.2.2 画布与图层

上面讲到了画布(Bitmap)、图层(Layer)和 Canvas 的概念,下面具体讲解下它们之间的关系。

  • 图层(Layer):每一次调用 canvas.drawXXX 系列函数时,都会生成一个透明图层来专门来绘制这个图形,比如前面在绘制矩形时的透明图层就是这个概念。
  • 画布(Bitmap):每块画布都是一个 Bitmap,所有的图像都是画在 Bitmap上的。我们知道,每次调用 canvas.drawXXX 函数时,都会生成一个专用的透明图层来绘制这个图形,绘制完成以后,就覆盖在画布上。所以,如果我们连续调用 5 个 draw 函数,就会生成 5 个透明图层,画完之后依次覆盖在画布上显示。画布有两种:第一种是 View 的原始画布,是通过 onDraw(Canvas canvas) 函数传入的,参数中的 canvas 就对应的是 View 的原始画布,控件的背景就是画在这块画布上的;另一种是人造画布,通过 saveLayer()、new Canvas(bitmap) 等函数来人为地新建一块画布。尤其是 saveLayer() 函数,一旦调用 saveLayer() 函数新建一块画布,以后的所有 draw 函数所画的图像都是画在这块画布上的,只有在调用 restore()、resoreToCount() 函数以后,才会返回到原始画布上进行绘制。
  • Canvas:Canvas 是画布的表现形式,我们所要绘制的任何东西都是利用 Canvas 来实现的。在代码中,Canvas 的生成方式只有一种——new Canvas(bitmap),即只能通过 Bitmap 生成,无论是原始画布还是人造画布,所有的画布最后都是通过 Canvas 画到 Bitmap 上的。可以把 Canvas 理解成绘图工具,利用它所封装的绘图函数来绘图,而所要绘制的内容最后是画在 Bitmap 上的。所以,如果我们利用 Canvas.clipXXX 系列函数将画布进行裁剪,其实就是把它对应的 Bitmap 进行裁剪,与之对应的结果就是再利用 Canvas 绘图的区域会减小。

9.2.3 saveLayer() 和 saveLayerAlpha() 函数的用法

1. saveLayer() 函数的用法

1
2
3
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayer(float left, float top, float right, float bottom,
Paint paint, int saveFlags)

参数:

  • RectF bounds:新建画布的尺寸。
  • Paint paint:画笔实例。
  • int saveFlags:新建画布的标识(详见 9.3 节)。

saveLayer() 函数会新建一块画布(Bitmap),后续的所有操作都是在这块画布上进行的。下面我们来看一下 saveLayer() 函数使用中的注意事项。

1)saveLayer() 函数后的所有动作都只对新建画布有小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestView extends View {
private Paint mPaint;
private Bitmap mBitmap;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.skew(1.732f, 0);
canvas.drawRect(0, 0, 100, 100, mPaint);
canvas.restoreToCount(layerId);
}
}

在 onDraw() 函数中,我们先在 View 的原始画布上画上了小狗的图像,然后利用 saveLayer() 函数新建了一个图层,然后利用 canvas.skew() 函数将新建的图层水平斜切 45°,所以之后画的矩形 (0,0,100,100) 就是斜切的。

而正是由于在新建画布后的各种操作都是针对新建画布进行的,所以不会对以前的画布产生影响。从效果图中也可以明显看出,将画布水平斜切 45° 也只影响了 saveLayer() 函数的新建画布,并没有对之前的原始画布产生影响。

2)通过 Rect 指定的矩形大小就是新建的画布大小。

在 saveLayer() 函数的参数中,可以通过指定 Rect 对象或者指定 4 个点来指定一个矩形,这个矩形的大小就是新建画布的大小。我们举例来看一下:

1
2
3
4
5
6
7
8
9
10
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
int layerId = canvas.saveLayer(0, 0, 200, 200, mPaint,
Canvas.ALL_SAVE_FLAG);
canvas.drawColor(Color.GRAY);
canvas.restoreToCount(layerId);
}

在绘图时,我们先把小狗图片绘制在原始画布上的,然后新建一个大小为 (0,0,200,200) 的透明画布,并将画布填充为灰色。由于画布大小只有 (0,0,200,200),所以从效果图中可以看出,也只有这一小部分区域被填充为灰色。

有些读者可能会想,为了避免画布太小而出现问题,每次都新建一块屏幕大小的画布多好。这样虽然是不会出现问题,但屏幕大小的画布需要多少存储空间呢?按一个像素需要 8bit 存储空间算,分辨率为 1024 像素 x 768 像素的机器,所占用的存储空间就是 1024 * 768 * 8 = 6.2MB。所以我们在使用 saveLayer() 函数新建画布时,一定要选择适当的大小,否则你的 APP 很可能 OOM。

注意:在前面示例中都是直接新建全屏画布的,这只是为了方便展示,在现实使用中一定要创建适当的画布大小。

2. saveLayerAlpha() 函数的用法

1
2
3
public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha(float left, float top, float right, float bottom,
int alpha, int saveFlags)

相比 saveLayer() 函数,多了一个 alpha 参数,用于指定新建画布的透明度,取值范围为 0~255,可以用十六进制的 0xAA 表示,取 0 时表示全透明。

这个函数的意义也是在调用的时候会新建一块画布,以后的各种绘图操作都作用在这个画布上,但这个画布是有透明度的,透明度就是通过 alpha 参数指定的。

将上述示例中的 saveLayer() 函数改为 saveLayerAlpha() 函数来重新作图。

1
2
3
4
5
6
7
8
9
10
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
int layerId = canvas.saveLayerAlpha(0, 0, 200, 200, 100,
Canvas.ALL_SAVE_FLAG);
canvas.drawColor(Color.WHITE);
canvas.restoreToCount(layerId);
}

在调用 saveLayerAlpha() 函数时,将新建画布的透明度设置为 100%,然后将画布同样填充为白色。从效果图中可以看出,在新建图像与上层画布合成以后,是具有透明度的。

愿化身那堵墙.gif